package com.patternity.apt;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import com.patternity.MetaModelScannerContext;
import com.patternity.MetaModelScannerContext.TypeKind;
import com.patternity.model.DesignDeclaration;
import com.sun.mirror.apt.AnnotationProcessor;
import com.sun.mirror.apt.AnnotationProcessorEnvironment;
import com.sun.mirror.declaration.AnnotationMirror;
import com.sun.mirror.declaration.AnnotationTypeElementDeclaration;
import com.sun.mirror.declaration.AnnotationValue;
import com.sun.mirror.declaration.ClassDeclaration;
import com.sun.mirror.declaration.Declaration;
import com.sun.mirror.declaration.FieldDeclaration;
import com.sun.mirror.declaration.InterfaceDeclaration;
import com.sun.mirror.declaration.MethodDeclaration;
import com.sun.mirror.declaration.Modifier;
import com.sun.mirror.declaration.PackageDeclaration;
import com.sun.mirror.declaration.ParameterDeclaration;
import com.sun.mirror.declaration.TypeDeclaration;
import com.sun.mirror.type.ArrayType;
import com.sun.mirror.type.DeclaredType;
import com.sun.mirror.type.InterfaceType;
import com.sun.mirror.type.PrimitiveType;
import com.sun.mirror.type.TypeMirror;
import com.sun.mirror.type.VoidType;

/**
 * The annotation processor to scan every annotation that represent design
 * intents declarations, and to create the simplified metamodel used by
 * Patternity.
 * 
 * This processor interacts with the rest of the framework through the
 * MetaModelScannerContext.
 * 
 * @author cyrille martraire
 */
public final class AnnotationScanner implements AnnotationProcessor {

	/**
	 * The given environment.
	 */
	private final AnnotationProcessorEnvironment env;

	/**
	 * Remembers which declarations have been visited already
	 */
	private final Map<Declaration, Object> visited = new HashMap<Declaration, Object>();

	/**
	 * The external context we interact with
	 */
	private final MetaModelScannerContext context = new MetaModelScannerContext();

	public AnnotationScanner(final AnnotationProcessorEnvironment env) {
		this.env = env;
	}

	@Override
	public void process() {
		for (final TypeDeclaration typeDecl : env
				.getSpecifiedTypeDeclarations()) {
			foreachType(typeDecl);
		}
		context.done();
	}

	private boolean isAlreadyVisited(final Declaration decl) {
		return visited.containsKey(decl);
	}

	private void foreachType(final TypeDeclaration typeDecl) {
		processDeclaration(typeDecl);
		processDeclaration(typeDecl.getPackage());

		collectSuperTypes(typeDecl);
		collectDependencies(typeDecl);
	}

	private void collectSuperTypes(final TypeDeclaration typeDecl) {
		final String qualifiedName = qualifiedName(typeDecl);

		if (typeDecl instanceof ClassDeclaration) {
			final ClassDeclaration classDecl = (ClassDeclaration) typeDecl;
			final String superClassQualifiedName = classDecl.getSuperclass()
					.getDeclaration().getQualifiedName();
			if (!superClassQualifiedName.equals("java.lang.Object")) {
				context.addSuper(qualifiedName, superClassQualifiedName);
			}
		}

		for (final InterfaceType interfaceType : typeDecl.getSuperinterfaces()) {
			final String interfaceQualifiedName = interfaceType
					.getDeclaration().getQualifiedName();
			context.addSuper(qualifiedName, interfaceQualifiedName);
		}
	}

	private void collectDependencies(final TypeDeclaration typeDecl) {
		final String qualifiedName = qualifiedName(typeDecl);
		for (final MethodDeclaration methodDecl : typeDecl.getMethods()) {
			context.addSuper(qualifiedName, toQName(methodDecl.getReturnType()));
			for (final ParameterDeclaration param : methodDecl.getParameters()) {
				context.addDependency(qualifiedName, toQName(param.getType()));
			}
		}
		for (final FieldDeclaration fieldDecl : typeDecl.getFields()) {
			context.addDependency(qualifiedName, toQName(fieldDecl.getType()));
		}
	}

	private static String toQName(final TypeMirror type) {
		if (type instanceof VoidType || type instanceof PrimitiveType) {
			return null;
		}
		if (type instanceof ArrayType) {
			return toQName(((ArrayType) type).getComponentType());
		}
		if (type instanceof DeclaredType) {
			return ((DeclaredType) type).getDeclaration().getQualifiedName();
		}
		return type.toString();
	}

	private void processDeclaration(final Declaration decl) {
		if (isAlreadyVisited(decl)) {
			return;
		}
		markAsVisited(decl);

		// add to model
		context.addElement(qualifiedName(decl), toKind(decl),
				decl.getDocComment());

		scanAnnotations(decl);
	}

	private static TypeKind toKind(final Declaration decl) {
		if (decl instanceof InterfaceDeclaration) {
			return TypeKind.INTERFACE;
		}
		if (decl.getModifiers().contains(Modifier.ABSTRACT)) {
			return TypeKind.ABSTRACT;
		}
		if (decl.getModifiers().contains(Modifier.FINAL)) {
			return TypeKind.FINAL;
		}
		return TypeKind.CONCRETE;
	}

	private Object markAsVisited(final Declaration decl) {
		return visited.put(decl, null);
	}

	/**
	 * Scan every annotation for this declaration, which is is visited for the
	 * first time.
	 */
	private void scanAnnotations(final Declaration decl) {
		final String qualifiedName = qualifiedName(decl);
		for (final AnnotationMirror ann : decl.getAnnotationMirrors()) {
			final String annotationQualifiedName = ann.getAnnotationType()
					.getDeclaration().getQualifiedName();
			final DesignDeclaration designDeclaration = new DesignDeclaration(
					qualifiedName, annotationQualifiedName, attributes(ann));
			addDeclaration(designDeclaration);
		}
	}

	private void addDeclaration(final DesignDeclaration designDeclaration) {
		context.addDeclaration(designDeclaration);
		// test assertion
		assertion(designDeclaration);
	}

	// Unit-test like assertion embedded here for simplicity
	// It it too hard to TDD for this piece so linked to APT!
	private void assertion(final DesignDeclaration designDeclaration) {
		if (designDeclaration.getName().equals(
				"com.martraire.cyrille.example.annotationset1.DummyAnnotation")) {
			final String expected = "{enumValue=NANOSECONDS, multipleClass=[com.martraire.cyrille.example.travelagency.VacationProduct], multipleText=[text1, text2], singleClass=com.martraire.cyrille.example.travelagency.Flight, singleText=text}";
			final Map<String, Object> attributes = new TreeMap<String, Object>(
					designDeclaration.getAttributes());
			if (!attributes.toString().equals(expected)) {
				final String message = "Parsing of annotation attributes is not as expected: \n"
						+ attributes.toString() + "\n" + expected;
				throw new AssertionError(message);
			}
		}
	}

	private static Map<String, Object> attributes(final AnnotationMirror ann) {
		final Map<String, Object> map = new HashMap<String, Object>();
		final Map<AnnotationTypeElementDeclaration, AnnotationValue> annotationValues = ann
				.getElementValues();

		for (final Entry<AnnotationTypeElementDeclaration, AnnotationValue> entry : annotationValues
				.entrySet()) {
			final Object value = entry.getValue().getValue();
			map.put(entry.getKey().getSimpleName().toString(), convert(value));
		}
		return map;
	}

	private static Object convert(final Object value) {
		if (value == null) {
			return null;
		}
		if (isMultipleAnnotationValue(value)) {
			return convertToList(value);
		}
		return convertToString(value);
	}

	private static boolean isMultipleAnnotationValue(final Object value) {
		return value instanceof List<?>;
	}

	/**
	 * Converts annotation values that are multiple (array) using
	 * convertToString() into List
	 */
	private static List<String> convertToList(final Object value) {
		final List<String> result = new ArrayList<String>();
		final List<?> list = (List<?>) value;
		for (final Object item : list) {
			final AnnotationValue annoValue = (AnnotationValue) item;
			result.add(convertToString(annoValue.getValue()));
		}
		return result;
	}

	/**
	 * Converts annotation values into their String representation, to string
	 * without quotes, to Class without ".class" at the end, or to enum constant
	 * ready to parse using valueOf()
	 */
	private static String convertToString(final Object value) {
		if (value == null) {
			return null;
		}
		if (value instanceof TypeMirror) {
			final TypeMirror mirror = (TypeMirror) value;
			return mirror.toString();
		}
		return value.toString();
	}

	/**
	 * @return The qualified name of the given element declaration, if possible,
	 *         or its simple name otherwise
	 */
	private static String qualifiedName(final Declaration decl) {
		if (decl instanceof TypeDeclaration) {
			final TypeDeclaration typeDecl = (TypeDeclaration) decl;
			return typeDecl.getQualifiedName();
		}
		if (decl instanceof PackageDeclaration) {
			final PackageDeclaration packageDecl = (PackageDeclaration) decl;
			return packageDecl.getQualifiedName();
		}
		return decl.getSimpleName();
	}
}